/*
 *  Copyright (C) 2004 Cidero, Inc.
 *
 *  Permission is hereby granted to any person obtaining a copy of 
 *  this software to use, copy, modify, merge, publish, and distribute
 *  the software for any non-commercial purpose, subject to the
 *  following conditions:
 *  
 *  The above copyright notice and this permission notice shall be included
 *  in all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
 *  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
 *  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY IN CONNECTION WITH THE SOFTWARE.
 * 
 *  File: $RCSfile: CDSObject.java,v $
 *
 */

package com.cidero.upnp;

import java.util.Vector;
import java.util.ArrayList;
import java.util.logging.Logger;

import javax.swing.ImageIcon;


import org.w3c.dom.DOMException;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import org.cybergarage.xml.XML;

/**
 *
 *  Base object for MediaServer Content Directory Service (CDS).
 *  Subclasses are UPNPMediaContainer and UPNPMediaItem
 * 
 *  This java class heirarchy maps more or less directly to the CDS XML
 *  version, and serves as the bridge between the Java and XML worlds
 *  
 */
public abstract class CDSObject implements Cloneable
{
  private static Logger logger = Logger.getLogger("com.cidero.upnp");

  //
  // Notes:
  //
  // Req. = required object property (must be included in DIDL-XML)
  // Opt. = not required. If browse/search filter property not set to '*' (all)
  // then object property should only be included in DIDL-XML if explicitly
  // requested in filter string
  //

  String id;           // Unique object id   (Req.)
  String parentId;     // Id of object's parent. Root parent = -1 (Req.)
  String title;        // Name of the object.   (Req.)
  String creator;      // Primary content creator or owner of the object (Opt.)


  //
  // List of resources (0 or more) associated with the object.  
  // Multiple resources are provided for to support multiple forms of
  // the same media object (such as thumbnail versions of images or 
  // different MIME types for the same music track)
  //
  Vector resourceList = new Vector(); 
  
  boolean restricted=false;  //  'true' -> ctrl point can't modify a given obj (Req.)
  String writeStatus;  // Write status of object (similar to dir perm) (Opt.) 
                       

  // UPNP class string (e.g. 'object.item.audioItem')   
  static String upnpClass = "object";
  static String xmlElementName = "object";

  //----------------------------------------------------------------
  // Extra (non-UPnP standard) object properties
  //----------------------------------------------------------------

  // Numerical id for object. This is useful when constructing the unique 
  // string ids stored in the UPNP 'objectId' field ('id' in this class).
  int numericalId = 0;

  // 'Track' number for abitrary objects (separate from 'originalTrackNumber'
  // in MusicTrack, Image, objects) 
  int trackNumber = 0;

  // When a particular resource is chosen out of the available set,
  // this variable can be set to that resource. Right now, if set,
  // the object XML output routines check for selectedResource != null 
  // and only output XML for that single resource
  CDSResource selectedResource = null;
  
  // Thumbnail image for object. If object is displayed in multiple windows
  // (views), it's handy to associate any accompanying thumbnail image with
  // the object itself (not maintain multiple copies in each view - though
  // that's still possible if desired)
  ImageIcon thumbImage = null;


  /** 
   * Simple constructor to create empty object
   */
  public CDSObject()
  {
  }

  public Object clone() 
  {
    try
    {
      // base class version - does bitwise copy. This is sufficient for
      // most fields, with the exception of the resource list, which is
      // mutable 
      CDSObject obj = (CDSObject)super.clone();

      // Do deep copy of resource list
      Vector cloneResourceList = new Vector(); 

      for( int n = 0 ; n < resourceList.size() ; n++ )
      {
        CDSResource thisResource = (CDSResource)resourceList.get(n);
        CDSResource cloneResource = (CDSResource)thisResource.clone();
        cloneResourceList.add( cloneResource );
      }
      obj.setResourceList( cloneResourceList );

      return obj;
    }
    catch( CloneNotSupportedException e )
    {
      // This should be impossible...
      logger.warning("Exception " + e );
      e.printStackTrace();
      return null;
    }
  }
  
  /**
   * Constructor to build object from a set of DIDL-Lite metadata nodes
   */
  public CDSObject( Node node )
  {
    // Incoming node is at the level 'container' or 'item'. Process
    // attributes first, then sub-elements
    
    NamedNodeMap attributes = node.getAttributes();

    String tmpString;
    
    // System.out.println("Processing attributes----------" + attributes );

    try
    {
      //
      // Allow these attr to be missing for now...(some devices don't set
      // them in all cases, such as DIDL-Lite returned from GetPosition
      // action)
      //
      if( attributes.getNamedItem("id") != null )
        id = attributes.getNamedItem("id").getNodeValue();

      if( attributes.getNamedItem("parentID") != null )
        parentId = attributes.getNamedItem("parentID").getNodeValue();

      if( attributes.getNamedItem("restricted") != null )
      {
        tmpString = attributes.getNamedItem("restricted").getNodeValue();
        restricted = toBoolean( tmpString );
      }
    }
    catch( DOMException e )
    {
      System.out.println( "Error getting required object property" +
                          e.getMessage() );
    }
    
    NodeList children = node.getChildNodes();

    // System.out.println("Processing children ----------" + children );

    for( int n = 0 ; n < children.getLength() ; n++ )
    {
      String nodeName = children.item(n).getNodeName();

      //System.out.println( "OBJ Node is: " + nodeName );
      
      if( nodeName.equals("dc:title") )
      {
        title = CDS.getSingleTextNodeValue( children.item(n) );
      }
      else if( nodeName.equals("dc:creator") )
      {
        creator = CDS.getSingleTextNodeValue( children.item(n) );
      }
      else if( nodeName.equals("upnp:writeStatus") )
      {
        writeStatus = CDS.getSingleTextNodeValue( children.item(n) );
      }
      else if( nodeName.equals("res") )
      {
        //System.out.println("ADDING RESOURCE!!!!!");
        CDSResource resource =
          new CDSResource( children.item(n) );
        
        resourceList.add( resource );
      }
    }
  }
  
  /**
   * Construct a CDS object from a URL metadata combination. 
   * 
   * @param   url        URL string. May be null if metadata contains
   *                     URL info in a resource
   *
   * @param   metadata   DIDL-Lite metadata string, or null if no 
   *                     metadata, in which case the CDS object's resource
   *                     is created solely from the URL.
   *
   * @return  CDS object version of the URL/metadata, or null if
   *          there was an error with the metadata
   */
  /*
  public CDSObject CDSObject( String url, String metaData )
  {
    this();
    
    if( metaData == null )
    {
      setTitle("Unknown");   // TODO: Use tail of URL 
      setCreator("Unknown");

      CDSResource resource = new CDSResource( url );
      addResource( resource );
    }
    else
    {
      CDSObjectList tmpObjList = new CDSObjectList( metaData );

      // Expect only a single object's worth of metadata (TODO: this may be
      // the wrong assumption - check)
      if( tmpObjList.size() == 1 )
      {
        obj = tmpObjList.getObject(0);

        // If the metadata included a valid resource, leave it alone. If
        // not, create a basic resource element from the url. 
        if( obj.getResource(0) == null )
        {
          CDSResource resource = new CDSResource();
          resource.setName( url );
          resource.setProtocolInfo( "http-get:*:audio/mpeg:*" );
          obj.addResource( resource );
        }
        return obj;
    }

    logger.warning("Error adding object - metadata described " + 
                   tmpObjList.size() + " objects instead of the expected 1");
    return null;
    
  }
  */

  public boolean toBoolean( String boolString ) 
  {
    if( boolString.equals("true") || (! boolString.equals("0")) )
      return true;
    else
      return false;
  }

  /* Set/Get methods */

  /**
   *  Set object id.
   *
   *  The id is unique with respect to the content directory. The UPNP 
   *  specification declares this as a string, so to be compatible with all 
   *  devices, use a string here (as opposed to integer)
   *
   *  @param  id    Unique id string 
   */
  public void   setId( String id ) { this.id = id; }

  /**
   *  Get object id
   *
   *  @return  Unique id string
   */
  public String getId() { return id; }


  /**
   *  Set parent object id.
   *
   *  The parent object id of the Content Directory root container must be 
   *  set to -1.
   * 
   *  @param  id    Unique id string 
   */
  public void setParentId( String id ) { this.parentId = id; }

  /**
   *  Get parent object id
   *
   *  @return  Unique id string
   */
  public String getParentId() { return parentId; }


  /**
   *  Set object title
   *
   *  @param  title    Title string. For a container, this will typically
   *                   be something like 'MyMusic', while for an item it
   *                   will typically be the audio track title (for example) 
   */
  public void setTitle( String title ) { this.title = title; }

  /**
   *  Get object title
   *
   *  @return  Title string
   */
  public String getTitle() { return title; }

  /**
   *  Set object creator
   *
   *  For media content items, this is the creator of the content (artist).
   *  For media containers, the creator property may often be undefined,
   *  or set to the container 'owner'
   *
   *  @param  creator    Creator string. 
   */
  public void setCreator( String creator ) { this.creator = creator; }

  /**
   *  Get object creator
   *
   *  @return  Creator string
   */
  public String getCreator() { return creator; }


  /**
   *  This base class doesn't contain 'artist' property, but it is often
   *  useful to access the artist property from the base class to 
   *  substitute it for the 'creator' property if it is not present.
   *
   *  This method defaults to null here, and is overridden in the 
   *  CDSMusicTrack/CDSMusicAlbum classes (which contain the 'artist' field)
   */
  public String getArtist() { return null; }
    
     
  /**
   *  Add a resource to an object
   *
   *  This is mostly used for items, not containers
   *
   *  Specify a -1 for integer args and null for string args to leave
   *  the attribute in the unitialized state (XML output formatter should 
   *  be set up to ignore uninitialized fields - TODO)
   */

  public void addResource( String name,
                           long size, String duration, int bitRate,
                           int sampleFreq, int bitsPerSample, 
                           int numAudioChannels, String resolution,
                           int colorDepth, String protocolInfo, 
                           String protection, String importURI ) 
  {
    CDSResource resource =
      new CDSResource( name,
                                  size, duration, bitRate,
                                  sampleFreq, bitsPerSample, 
                                  numAudioChannels, resolution,
                                  colorDepth, protocolInfo, 
                                  protection, importURI );
    
    resourceList.add( resource );
  }
  

  /**
   *  Add a resource to an object, using an externally declared resource
   *
   *  Specify a -1 for integer args and null for string args to leave
   *  the attribute in the unitialized state (XML output formatter should 
   *  be set up to ignore uninitialized fields - TODO)
   *
   *  @param  Resource object
   */
  public void addResource(  CDSResource resource ) {
    resourceList.add( resource );
  }
  
  public void clearResources( ) {
    resourceList.clear();
  }
  
  public CDSResource getResource( int index ) {
    return (CDSResource)resourceList.get( index );
  }

  public int getResourceCount( ) {
    return resourceList.size();
  }

  /**
   *  Needed to support clone() - (resourceList is mutable)
   */
  public void setResourceList( Vector resourceList ) {
    this.resourceList = resourceList;
  }

  public void setSelectedResource( CDSResource res ) {
    selectedResource = res;
  }


  /**
   *  Get object class
   *
   *  @return  UPNP class string
   */
  public String getUPNPClass() { return upnpClass; }

  
  /**
   *  Set object 'restricted' property
   *
   *  When set to 'true', ability to modify a given object is confined 
   *  to the Content Directory Service. Control point metadata write 
   *  access is disabled.
   *
   *  @param  restricted    Class string. 
   */
  public void setRestricted( boolean restricted ) {
    this.restricted = restricted;
  }

  /**
   *  Get value of 'restricted' property
   *
   *  @return  restricted setting
   */
  public boolean getRestricted() { return restricted; }


  /**
   *  Set object write status
   *
   *  When present, controls the modifiability of the resources of a given
   *  object. 
   *
   *  @param  writeStatus   Valid values are WRITABLE, PROTECTED,
   *                        NOT_WRITABLE, UNKNOWN, or MIXED.
   */
  public void setWriteStatus( String writeStatus ) {
    this.writeStatus = writeStatus;
  }

  /**
   *  Get write status property
   *
   *  @return  writeStatus   Valid values are WRITABLE, PROTECTED,
   *                         NOT_WRITABLE, UNKNOWN, or MIXED.
   */
  public String getWriteStatus() { return writeStatus; }


  //-------------------------------------------------------------------
  //  Extra tags that have proven useful for applications (not part of 
  //  UPnP AV standard). 
  //-------------------------------------------------------------------

  public void setNumericalId( int numericalId ) {
    this.numericalId = numericalId;
  }
  public int getNumericalId() { return numericalId; }

  /**
   *  Non-UPnP-standard track number tag (UPnP tag is 'originalTrackNumber'
   *  useful for renderers keeping track of 'track numbers' for arbitrary
   *  CDS objects (not necessarily music tracks)
   */
  public void setTrackNumber( int trackNumber ) {
    this.trackNumber = trackNumber;
  }
  public int getTrackNumber() { return trackNumber; }

  //-------------------------------------------------------------------
  //  End of extra tags
  //-------------------------------------------------------------------

  public String getObjectXMLElementName() { return xmlElementName; }

  /**
   *  Generate XML version of object
   *
   *  This routine is set up to walk the object tree of the derived class,
   *  adding attributes/elements of each class in a 'bottom-up' manner 
   *  (base object attributes/elements are output first)
   *
   *  Each derived class needs to implement the attributesToXML() and
   *  elementsToXML() methods, and those methods should call 
   *  super.attributesToXML()/super.elementsToXML() prior to adding
   *  their own stuff to the the XML string.
   */
  public String toXML( CDSFilter filter )
  {
    return "<" + getObjectXMLElementName() + attributesToXML( filter ) + ">\n" +
              elementsToXML( filter ) + 
           "</" + getObjectXMLElementName() + ">";
  }

  public String attributesToXML( CDSFilter filter )
  {
    // All these properties are required, so don't need to use filter obj here.
    // Just included for consistency with same call in derived obj
    String attributeXML = 
      " id=\"" + id + 
      "\" parentID=\"" + parentId + 
      "\" restricted=\"" + restricted + "\"";

    return attributeXML;
  }

  public String elementsToXML( CDSFilter filter )
  {
    StringBuffer buf = new StringBuffer();
    
    buf.append( "  <dc:title>");   // Required
    buf.append( XML.escapeXMLChars(title) );
    buf.append( "</dc:title>\n" );
    buf.append( "  <upnp:class>" );  // Required
    buf.append( getUPNPClass() );
    buf.append( "</upnp:class>\n" );

    // Optional elements

    if( creator != null && filter.propertyEnabled( "dc:creator" ) )
    {
      buf.append( "  <dc:creator>" );
      buf.append( XML.escapeXMLChars(creator) );
      buf.append( "</dc:creator>\n" );
    }
    
    if( writeStatus != null && filter.propertyEnabled( "upnp:writeStatus" ) )
    {
      buf.append( "  <upnp:writeStatus>" );
      buf.append( writeStatus );
      buf.append( "</upnp:writeStatus>\n" );
    }

    if( filter.propertyEnabled("res") )
    {
      if( selectedResource != null )
      {
        buf.append( selectedResource.toXML( filter ) );
      }
      else
      {
        CDSResource resource;

        for( int n = 0 ; n < resourceList.size() ; n++ )
        {
          resource = (CDSResource)resourceList.get(n);
          buf.append( resource.toXML( filter ) );
        }
      }
    }
    
    return buf.toString();
  }

  public String toDIDL()
  {
    StringBuffer buf = new StringBuffer();
    
    buf.append( CDS.DIDL_LITE_HEADER );
    
    // Set up filter object to output *all* properties (required and optional)
    CDSFilter filter = new CDSFilter("*");

    buf.append( toXML( filter ) );
    buf.append( "\n" );
    buf.append( CDS.DIDL_LITE_TRAILER );

    return buf.toString();
  }

  abstract public boolean isContainer();
  public boolean isItem()
  {
    return ( !isContainer() );
  }

  
  /**
   *  Return simplest string representation of an object for now. This method
   *  is called by the MediaController code in the content tree rendering 
   *  window, so if you change it, beware - you will have to change how
   *  that code works! 
   */
  public String toString()
  {
    return title;
  }
  
  /**
   *  Return the best resource match to the specified protocol list,
   *  assuming that 'earlier' protocols in the list are preferred
   *
   *  Note: basic algorithm lifted from the Intel NMRP2.0 guidelines
   *  doc (Appendix D).
   *
   *  @param protcolInfo    Array of supported protocols, with 'preferred'
   *                        protocols earlier in the list
   *
   *  @param ipAddr         IP address filter (NOT YET IMPL - TODO)
   *
   *  @param preferred      Preferred image resolutions. These are
   *                        currently weighted more heavily than the 
   *                        supported protocol ordering, so an exact
   *                        size match will have a big impact
   *
   *  @param losslessWMATranscodeThreshBitsPerSec
   *  
   *                        Threshold at which to assume a WMA is a 
   *                        lossless file, and the transcoded PCM resource
   *                        should be selected if available and lossless
   *                        is not natively supported by the device.
   *                        If -1, no special logic is used for WMA resources
   *                        (the default for renderers that support 
   *                        lossless WMA natively) 
   *
   *  @return  Best matching resource, or null if no match found
   *
   *  Notes:  IP matching logic not yet enabled - most users will have
   *          only a single network segment initially, so this ok for now
   *          (TODO: Need to add IP match though)
   *  
   */
  public CDSResource
  getBestMatchingResource( ArrayList protocolInfoList,
                           String ipAddr,
                           String preferredImageResolution,
                           int losslessWMATranscodeThreshBitsPerSec )
  {
    long bestIpMatchScore = 0;    //best bitwise-AND match on IP address
    long bestProtoInfoMatchScore = 0; //best protocolInfo string match
    CDSResource bestMatch = null; // best matched resource, start with none
    CDSResource res;

    int resourceCount = getResourceCount();
    if( resourceCount <= 0 )
    {
      logger.warning("Object has no resources");
      return null;
    }

    if( protocolInfoList.size() == 0 )
    {
      logger.warning("Device has empty list of supported protocols!");
      return null;
    }

    if( preferredImageResolution != null )
    {
      for( int n = 0 ; n < resourceCount ; n++ )
      {
        res = getResource(n);

        if( (res.getResolution() != null) && 
            res.getResolution().equalsIgnoreCase(preferredImageResolution) )
        {
          logger.fine("Found exact resolution match (" + 
                      preferredImageResolution + ") Proto = " + 
                      res.getProtocolInfo() );

          if( isSupportedProtocol( protocolInfoList, res.getProtocolInfo() ) )
          {
            logger.fine("Protocol is supported!" );
            return res;
          }
        }
      }
    }

    //
    // If device doesn't support lossless WMA natively, the selection 
    // algorithm needs to be smart about detecting lossless WMA's and
    // selecting the transcoded PCM version of the resource instead.
    //
    // The selection algorithm (similar logic to Roku Soundbridge) is
    // to look at the WMA bitrate, and if it is over a given value
    // (nominally 400KBits/sec, but configurable) then the PCM transcoded
    // version of the resource is selected instead, if available.
    //

    for( int n = 0 ; n < resourceCount ; n++ )
    {
      long ipMatchScore;        // metric value of the IP match
      long protoInfoMatchScore; // metric value of the protoInfo match

      res = getResource(n);

      //
      // Start protocolInfo match metric with highest value.
      // This gives more weight to protocolInfo that are listed earlier 
      // in the target renderer s protocolInfo list. 
      //
      protoInfoMatchScore = protocolInfoList.size() + 1; 

      // Iterate through the target's protocolInfo strings and break 
      // on the first protocolInfo that matches, decrementing piMatch 
      // on each iteration.
      for( int p = 0 ; p < protocolInfoList.size() ; p++ )
      {
        protoInfoMatchScore--;
        
        String deviceProtocolInfo = (String)protocolInfoList.get(p);

        //logger.fine("Checking device proto '" + deviceProtocolInfo +
        //             "' against resource proto '" + res.getProtocolInfo() +
        //             "'" );
        if( protocolInfoMatch( deviceProtocolInfo, res.getProtocolInfo() ) )
        {
          // Note UPnP 'BitRate' is really bytes/sec!!
          if( (losslessWMATranscodeThreshBitsPerSec >= 0) && 
              res.isWMA() && 
              ((res.getBitRate()*8) > losslessWMATranscodeThreshBitsPerSec) )
          {
            logger.info("Using transcoded WAV version of lossless WMA track");
          }
          else if ( protoInfoMatchScore > bestProtoInfoMatchScore )
          {
            bestProtoInfoMatchScore = protoInfoMatchScore;
            bestMatch = res;

            //logger.info("New best match! res = " + res.toString() );
          }
          break;
        }
        else
        {
          // Sometimes device supports extended version of the resources
          // protocol.  By convention (my observation, not necessarily 
          // accurate), these MIME types seems to all have a 'x-' prefix.
          // For example, a device that supports the extended version of 
          // the http-get:*:audio/mpegurl:* protocol may only advertise 
          // support for http-get:*:audio/x-mpegurl:*  .  See if this
          // is the case here, and declare a match if so
          String nonExtProtocolInfo = deviceProtocolInfo.replaceAll("x-", "");
          
          if( nonExtProtocolInfo.equalsIgnoreCase( res.getProtocolInfo() ) )
          {
            if ( protoInfoMatchScore > bestProtoInfoMatchScore )
            {
              bestProtoInfoMatchScore = protoInfoMatchScore;
              bestMatch = res;
            }
            break;
          }
        }
      }

      /*
      // Get the IP address of the resource s URI and compare 
      // it with the target renderer s IP address. Assume that the 
      // individual bytes (in the 4-byte representation) of the 
      // IP addresses corresponds to the order of an IP addresss
      // quad representation.
      IPAddress ipaddr = GetIPAddressBytes(res.ContentUri);
      ipMatch = target.IPAddress & ipaddr.Address;

      // If the metrics indicate a better match, then save the resource 
      // that would be the better match. Give more weight to 
      // the metric for IP address, as having a network route is more 
      // fundamental in an environment with multiple media formats. 
      // However, be sure to allow better protocolInfo match to take 
      // precedence when the IP address metric is the same.
      if ( (ipMatch > bestIpMatch) || 
           ((ipMatch == bestIpMatch) && (piMatch > bestProtInfoMatch)) )
      {
        bestMatch = res;
        bestIpMatch = ipMatch;
        bestProtInfoMatch = piMatch;
      }
      */

    }

    // Iterated through all resources, so return the best one.

    //
    // Hack - If none found yet, object type is audioBroadcast, and
    // protocol for one of the resources is 'application/octet-stream',
    // override the resource protocol with 'audio/mpeg', and return
    // it as a match.
    // 
    // (TVersity/Roku patch) - TODO: unhack this sometime
    //
    if( bestMatch == null )
    {
      if( this instanceof CDSAudioBroadcast )
      {
        for( int n = 0 ; n < resourceCount ; n++ )
        {
          res = getResource(n);
          if( res.getProtocolInfo().indexOf("application/octet-stream") > 0 )
          {
            logger.info("Overriding MIME type 'application/octet-stream' with 'audio/mpeg'");
            res.setProtocolInfo( "http-get:*:audio/mpeg:*" );
            return res;
          }
        }
      }
    }

    return bestMatch;
  }
  
  public boolean isSupportedProtocol( ArrayList protocolInfoList,
                                      String protocolInfo )
  {
    String deviceProtocolInfo = "Empty";

    for( int p = 0 ; p < protocolInfoList.size() ; p++ )
    {
      deviceProtocolInfo = (String)protocolInfoList.get(p);
      if( deviceProtocolInfo.equalsIgnoreCase( protocolInfo ) )
        return true;
    }
    logger.warning("Unsupported protocol! - " + protocolInfo +
                   " Last proto: " + deviceProtocolInfo );
    return false;
  }

  /**
   *  Match up a pair of protocolInfo strings. Only the first 3 sub-fields
   *  of the protocolInfo string are used, since the 4th field can have
   *  extra info not relevant to the actual protocol (DLNA additions for
   *  example)
   *  
   *  @note MIME types can have semicolon-separated fields within any
   *  single colon-separated field. An example of this is the audio/L16
   *  type used by Windows Media Player 11. It looks something like:
   *
   *    http-get:*:audio/L16;rate=44100;channels=2:*
   *   
   */
  public boolean protocolInfoMatch( String protoInfo1, String protoInfo2 )
  {
    String[] tmpProto = protoInfo1.split(":");
    String protoInfo1Base = tmpProto[0] + ":" + tmpProto[1] + ":";
    int index = tmpProto[2].indexOf(";");
    if( index > 0 )
      protoInfo1Base += tmpProto[2].substring(0,index);
    else
      protoInfo1Base += tmpProto[2];

    tmpProto = protoInfo2.split(":");
    String protoInfo2Base = tmpProto[0] + ":" + tmpProto[1] + ":";
    index = tmpProto[2].indexOf(";");
    if( index > 0 )
      protoInfo2Base += tmpProto[2].substring(0,index);
    else
      protoInfo2Base += tmpProto[2];


    //logger.info("proto1,2 base = " + protoInfo1Base + ", " +
    //             protoInfo2Base );
    if( protoInfo1Base.equalsIgnoreCase( protoInfo2Base ) )
      return true;

    return false;
  }

  public void setThumbImage( ImageIcon imageIcon ) {
    thumbImage = imageIcon;
  }
  public ImageIcon getThumbImage() {
    return thumbImage;
  }
  
}
